一文吃透 Spring @Transactional 全部失效场景,避开99%线上坑
前言
做 Java 开发没人不用 @Transactional,但线上经常遇到:明明加了事务注解,报错却不回滚。
大部分人只知道同类自调用失效,其实总共有 8 大失效场景,本文一次性全部整理齐全、附带原理+错误示例,收藏一篇就够用一辈子。
一、核心底层前提(先懂原理再记场景)
Spring 事务基于 AOP 动态代理:
- 只有外部通过代理对象调用带
@Transactional的方法,才会生效 - 不走代理、不被 AOP 拦截,事务直接失效
所有失效场景,本质都是:没走到 AOP 代理拦截链。
场景1:同类内部方法直接调用(this 自调用)
错误示例
@Service
public class OrderService {
public void createOrder() {
// 原生this调用,不走代理
this.saveOrder();
}
@Transactional(rollbackFor = Exception.class)
public void saveOrder() {
// 入库 + 抛异常
orderMapper.insert(...);
int a = 1 / 0;
}
}失效原因
this 是原始对象,不是 Spring 代理 Bean,AOP 无法拦截,事务注解无效。
解决
- 本类自注入(生产最常用)
@Autowired
private OrderService self;使用 self.方法() 调用,走代理。 2. AopContext 获取代理 启动类添加 @EnableAspectJAutoProxy(exposeProxy = true)
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.saveOrder();- 业务拆分:把事务方法抽至另外 Service,彻底避免同类调用。
场景2:方法不是 public 修饰
错误示例
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
void saveOrder() {
// 默认包访问权限 / private / protected 都不行
}
}失效原因
Spring AOP 动态代理只拦截 public 方法,非 public 无法生成代理增强,事务直接失效。
解决
事务方法强制改为 public,不要使用 private、protected、包级私有。
场景3:异常不是「受检/运行时异常」,未配置 rollbackFor
错误认知
默认 @Transactional 只回滚 RuntimeException + Error,普通 Checked 异常不回滚。
错误示例
@Transactional
public void biz() throws Exception {
// 抛出普通 Exception,不会回滚
throw new Exception("业务失败");
}失效原因
Spring 默认规则:
- 抛出
RuntimeException / Error→ 回滚 - 抛出 Checked Exception → 只提交不回滚
正确写法
// 生产统一标配
@Transactional(rollbackFor = Exception.class)场景4:try-catch 捕获了异常,没往外抛
错误示例
@Transactional(rollbackFor = Exception.class)
public void save() {
try {
userMapper.insert(...);
int i = 1 / 0;
} catch (Exception e) {
// 异常被吞,Spring 感知不到
log.error("出错", e);
}
}失效原因
异常被当前方法 catch 吃掉,没有抛给 AOP 事务拦截器,Spring 认为正常执行,直接提交事务。
解决
- 重新抛出异常(推荐)
catch (Exception e){
log.error("异常",e);
throw e;
}- 手动强制回滚(不抛异常也能回滚)
catch (Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}场景5:被 final / static 修饰的事务方法
错误示例
@Transactional(rollbackFor = Exception.class)
public final void saveUser() { ... }
@Transactional(rollbackFor = Exception.class)
public static void saveUser() { ... }失效原因
final:方法不能被重写,JDK/CGLIB 都无法代理增强static:属于类不属于实例,代理对象无法覆盖 static 方法
解决
事务方法严禁加 final、static。 不要把事务写在工具类、静态工具方法中。
场景6:类没有被 Spring 托管(没交給容器)
错误示例
// 没有 @Service / @Component
public class UserService {
@Transactional
public void save() { ... }
}
// 自己 new 对象调用
UserService service = new UserService();
service.save();失效原因
自己 new 的是普通对象,不是 Spring Bean,没有经过 AOP 代理,事务完全不生效。
解决
给当前类添加 @Service 或 @Component,必须由Spring注入Bean,禁止手动new。
场景7:事务传播行为配置不当导致不回滚
典型:REQUIRES_NEW、SUPPORTS、NOT_SUPPORTED 等用错。
示例:外层无事务,内层 SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void biz() {
// 外层没事务,自身以非事务运行,报错不回滚
int i = 1/0;
}常见坑
Propagation.NOT_SUPPORTED:始终以非事务执行Propagation.SUPPORTS:有事务就加入,没事务就非事务运行
解决
- 普通增删改一律使用默认 REQUIRED;
- 查询、统计接口使用
NOT_SUPPORTED; - 不要随意乱用传播属性,极易造成事务失效。
场景8:多线程异步调用事务方法
错误示例
@Transactional(rollbackFor = Exception.class)
public void save() {
new Thread(() -> {
// 子线程数据库操作,不在当前事务上下文
userMapper.insert(...);
}).start();
}失效原因
Spring 事务基于 ThreadLocal 绑定当前事务上下文,子线程无法继承父线程事务,各自独立事务,异常互不回滚。
解决
- 禁止在事务中开启子线程操作数据库;
- 多线程业务拆分独立事务;
- 分布式一致性使用 Seata、MQ 最终一致性方案。
总结:8大失效场景一句话汇总
- 同类 this 内部调用,未走代理;
- 方法非 public 修饰,AOP 不拦截;
- 抛出受检异常,没配置 rollbackFor;
- 异常被 try-catch 吞掉不外抛;
- 方法被 final / static 修饰;
- 手动 new 对象,类未交给 Spring 托管;
- 误用 SUPPORTS / NOT_SUPPORTED 传播级别;
- 多线程跨线程操作,ThreadLocal 事务隔离。
收尾建议
给大家贴一份生产通用事务模板,日常业务直接照搬,避免踩坑:
@Transactional(rollbackFor = Exception.class)
public void businessMethod(){
// 数据库增删改业务
}日常开发只要守住这 8 条规则,基本能杜绝 100% 事务莫名不回滚的线上问题。